package dbexp.utils;


import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Klasa dostarcza mapy właściwości.<br>
 * Opcjonalnie działanie według wzorca obserwatora.<br>
 * Rozszerza obiekt o dodatkowe właściwości, które mogą być używane zamiast dodawania nowych metod
 */
public class Container {
    /** Mapa do dodatkowych właściwości obiektu  */
	private Map<String,Object> values  = new HashMap<String,Object>();
    /** Mapa do nasłuchu dla operacji dodawanie, modyfikacji, czytania i usuwania obiektu */
	private Map<String, ContainerListener<?>> listeners  = new HashMap<String, ContainerListener<?>>();
	/** Mapa do nasłuchu dla operacji dodawanie, modyfikacji, czytania i usuwania obiektu konkretnej klasy */
	private Map<Class<?>, ContainerListener<?>> classlisteners  = new HashMap<Class<?>, ContainerListener<?>>();
	/** Nasłuch dla operacji dodawanie, modyfikacji, czytania i usuwania wszystkich obiektów kontenera */
	private ContainerListener<Object> listener  = null;

	/**
	 * Konstruktor domyślny
	 */
	public Container(){}
	
	/**
	 * Konstruktor dodający możliwość ustawienie nasłuchu dla wszystkich obiektów tego kontenera
	 * @param cl
	 */
	public Container(ContainerListener<Object> cl){
		addListener(cl);
	}
    /**
     * Ustawia dodatkową właściwość typu char
     * @param key klucz
     * @param v wartość char
     * @return this
     */
    public Container set(String key, char v){
    	setString(key, String.valueOf(v));return this;
    }
    /**
     * Ustawia dodatkową właściwość typu String::object.toString.<br>
     * W przypadku kiedy obiekt wskazuje na null, zostaje dodany napis "null"
     * @param key klucz
     * @param v obiekt
     * @return this
     */
    public <T>Container setString(String key, T v){
    	p(key, String.valueOf(v));
    	return this;
    }
    
    /**
     * Ustawia dodatkową właściwość dowolnego typu
     * @param key klucz
     * @param v wartość object
     * @return this
     */
    public <T>Container set(String key, T v){
    	if(v instanceof String){
    		p(key, (String)v);
		}else{
			p(key, v);
		}
    	return this;
    }
    /**
     * Ustawia dodatkową właściwość typu List
     * @param key klucz
     * @param v wartość object
     * @return this
     */
    @SuppressWarnings("unchecked")
	public <T>Container addToList(String key,final T v){
		List<T> pre = getList(key);
		if(pre == null){
			pre = new ArrayList<T>();
		}
		if(v instanceof List<?>){
			pre.addAll((List<T>)v);
		}else{
			pre.add(v);
		}
    	p(key, pre);
    	return this;
    }
     /**
     * Ustawia dodatkową właściwość typu List
     * @param key klucz
     * @param v wartość object
     * @return this
     */
    public Container addToList(String key,final String v){
		List<String> pre = getList(key);
		if(pre == null){
			pre = new ArrayList<String>();
		}
		pre.addAll((Collection<? extends String>) StringUtil.getList(v));
    	p(key, pre);
    	return this;
    }
    /**
     * Ustawia dodatkową właściwość typu boolean
     * @param key klucz
     * @param v wartość boolean
     * @return this
     */
    public Container set(String key, boolean v){
    	p(key, Boolean.valueOf(v));return this;
    }
    /**
     * Ustawia dodatkową właściwość typu int
     * @param key klucz
     * @param v wartość int
     * @return this
     */
    public Container set(String key, int v){
    	p(key, new Integer(v));return this;
    }
    /**
     * Ustawia dodatkową właściwość typu long
     * @param key klucz
     * @param v wartość long
     * @return this
     */
    public Container set(String key, long v){
    	p(key, new Long(v));return this;
    }
    /**
     * Ustawia dodatkową właściwość typu float
     * @param key klucz
     * @param v wartość float
     * @return this
     */
    public Container set(String key, float v){
    	p(key, new Float(v));return this;
    }
    /**
     * Ustawia dodatkową właściwość typu double
     * @param key klucz
     * @param v wartość double
     * @return this
     */
    public Container set(String key, double v){
    	p(key, new Double(v));return this;
    }
	/**
	 * Czy obiekt zawiera dodatkową właściwości typu boolean o nazwie key z wartością true
	 * @param key klucz
	 * @return get(key)==true
	 */
	public boolean is(String key){
		return is(key, false);
	}

	/**
	 * Czy obiekt zawiera dodatkową właściwości typu boolean o nazwie key z wartością true.<br>
	 * W przeciwnym razie zwraca wartość domyślną def.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i porównywana do true
	 * @param key klucz
	 * @param def wartość domyślna
	 * @return get(key)==true
	 */
	public boolean is(String key,boolean def){
		Object o = get(key);
		if(o instanceof Boolean){
			return ((Boolean)o).booleanValue();
		}
		if(o instanceof String){
			final String s = (String)o;
			return (s.trim().length()>0 && s.toLowerCase().equals("true"));
		}
		return def;
	}

	/**
	 * Pobiera dodatkową właściwość typu String lub reprezentację metody object.toString() lub null jeżeli nie mamy dodatkowej właściwości 
	 * @param k klucz
	 * @return get(k)::String
	 */
	public String getString(String k){
		return getString(k, null);
	}

	/**
	 * Pobiera dodatkową właściwość typu String lub reprezentację metody object.toString() lub null jeżeli nie mamy dodatkowej właściwości
	 * @param k klucz
	 * @return get(k)::String
	 */
	public String getString(String k, String def){
		final Object o = get(k);
		if(o!=null){
			if(o instanceof String){
				return (String)o;
			}
			return o.toString();
		}
		return def;
	}

	/**
	 * Pobiera dodatkową właściwość typu int.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ int.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca 0
	 * @param k klucz
	 * @return get(k)::int
	 */
	public int getInt(String k){
		return getInt(k, 0);
	}

	/**
	 * Pobiera dodatkową właściwość typu int.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ int.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca def
	 * @param k klucz
	 * @param def wartość domyślna
	 * @return get(k)::String
	 */
	public int getInt(String k, int def){
		final Object o = get(k);
		if(o instanceof Integer){
			return ((Integer)o).intValue();
		}
		if(o!=null){
			final String s = o.toString();
			if(s.trim().length()>0){
				try {
					return Integer.parseInt(s);
				} catch (NumberFormatException e) {
					return def;
				}
			}
		}
		return def;
	}

	/**
	 * Pobiera dodatkową właściwość typu long.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ long.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca 0
	 * @param k klucz
	 * @return get(k)::long
	 */
	public long getLong(String k){
		return getLong(k, 0);
	}

	/**
	 * Pobiera dodatkową właściwość typu long.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ long.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca def
	 * @param k klucz
	 * @param def wartość domyślna
	 * @return get(k)::long
	 */
	public long getLong(String k, long def){
		final Object o = get(k);
		if(o instanceof Long){
			return ((Long)o).longValue();
		}
		if(o!=null){
			final String s = o.toString();
			if(s.trim().length()>0){
				try {
					return Long.parseLong(s);
				} catch (NumberFormatException e) {
					return def;
				}
			}
		}
		return def;
	}

	/**
	 * Pobiera dodatkową właściwość typu float.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ float.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca 0
	 * @param k klucz
	 * @return get(k)::float
	 */
	public float getFloat(String k){
		return getFloat(k, 0);
	}

	/**
	 * Pobiera dodatkową właściwość typu float.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ float.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca def
	 * @param k klucz
	 * @param def wartość domyślna
	 * @return get(k)::float
	 */
	public float getFloat(String k, float def){
		final Object o = get(k);
		if(o instanceof Float){
			return ((Float)o).floatValue();
		}
		if(o!=null){
			final String s = o.toString();
			if(s.trim().length()>0){
				try {
					return Float.parseFloat(s);
				} catch (NumberFormatException e) {
					return def;
				}
			}
		}
		return def;
	}

	/**
	 * Pobiera dodatkową właściwość typu double.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ double.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca 0
	 * @param k klucz
	 * @return get(k)::double
	 */
	public double getDouble(String k){
		return getDouble(k, 0);
	}

	/**
	 * Pobiera dodatkową właściwość typu Double.<br>
	 * W przypadku jeżeli właściwość jest typu String jest wyciągana reprezentacja ciągu znakowego i parsowana na typ double.<br>
	 * W przypadku błędu parsowania lub wartości null zwraca def
	 * @param k klucz
	 * @param def wartość domyślna
	 * @return get(k)::double
	 */
	public double getDouble(String k, double def){
		final Object o = get(k);
		if(o instanceof Double){
			return ((Double)o).doubleValue();
		}
		if(o!=null){
			final String s = o.toString();
			if(s.trim().length()>0){
				try {
					return Double.parseDouble(s);
				} catch (NumberFormatException e) {
					return def;
				}
			}
		}
		return def;
	}
	/**
	 * Pobiera dodatkową właściwość typu List.<br>
	 * W przypadku jeżeli właściwość jest typu String jest tworzona nowa lista.<br>
	 * @param k klucz
	 * @return get(k)::List
	 */
	public <T>List<T> getList(String k){
		return getList(k, new ArrayList<T>());
	}

	/**
	 * Pobiera dodatkową właściwość typu List.<br>
	 * W przypadku jeżeli właściwość jest typu String jest tworzona nowa lista.<br>
	 * W przypadku wartości null zwraca def
	 * @param k klucz
	 * @param def wartość domyślna
	 * @return get(k)::List
	 */
	@SuppressWarnings("unchecked")
	public <T>List<T> getList(String k, List<T> def){
		final Object o = get(k);
		try{
			if(o instanceof List<?>){
				return toValue(o);
			}
			if(o!=null){
				return new ArrayList<T>(){{add((T) o);}};
			}
		}catch(Exception e){}
		return def;
	}
	/**
	 * Pobiera dodatkową właściwość typu Object lub null jeżeli nie mamy dodatkowej właściwości
	 * @param k klucz
	 * @return get(key)::Object
	 */
	public <T>T get(String k){
		return (T)(get(k, null));
	}

	/**
	 * Dodaje nasłuch dla operacji wykonywanych na wszystkich obiektach w kontenerze
	 * @param <T>
	 * @return this
	 */
	public Container addListener(ContainerListener<Object> cl){
		listener=(ContainerListener<Object>) cl.set(this);
		return this;
	}
	/**
	 * Dodaje nasłuch dla operacji wykonywanych na obiekcie
	 * @param key klucz obiektu
	 * @param cl obiekt nasłuchu
	 * @return this
	 */
	public <T>Container addListener(Class<T> key, ContainerListener<T> cl){
		classlisteners.put(key,  cl.set(this));
		return this;
	}
	/**
	 * Dodaje nasłuch dla operacji wykonywanych na obiekcie
	 * @param key klucz obiektu
	 * @param cl obiekt nasłuchu
	 * @return this
	 */
	public <T>Container addListener(String key, ContainerListener<T> cl){
		listeners.put(key, cl.set(this));
		return this;
	}
	/**
	 * Usuwa nasłuch nałożony na kontene dla wszystkich jego obiektów
	 * @return this
	 */
	public Container removeListener(){
		listener=null;
		return this;
	}
	/**
	 * Sprawdza czy kontener ma aktywny nasłuch
	 * @return ma nasłuch
	 */
	public boolean hasListener(){
		return listener!=null;
	}
	/**
	 * Usuwa nasłuch nałożony na obiekcie
	 * @param key klucz obiektu
	 * @return this
	 */
	public Container removeListener(String key){
		listeners.remove(key);
		return this;
	}
	/**
	 * Sprawdza czy kontener ma aktywny nasłuch na obiekcie
	 * @param key klucz obiektu
	 * @return ma nasłuch
	 */
	public boolean hasListener(String key){
		return listeners.get(key)!=null;
	}
	/**
	 * Ustawia dodatkową właściwość dowolnego typu
	 * @param k klucz
	 * @param v wartość Object
	 * @return this
	 */
	private <T>Container p(String k, T v){
		if(k!=null){
			T toInsert = v;
			if(listener!=null){
				if(has(k)){
					if(values.get(k).getClass().equals(v.getClass()))
					toInsert= (T)(listener.onUpdate(values.get(k), v));
				}else{
					toInsert= (T)(listener.onInsert(v));
				}
			}
			if(classlisteners!=null){
				Class<T> vclass = getClass(v);
				if(vclass!=null){
					if(classlisteners.size()>0){
						Class<T> svclass = toValue(vclass.getSuperclass());
						T vg = (T)(values.get(k));
						boolean has = has(k);
						for (Map.Entry<Class<?>, ContainerListener<?>> entry : classlisteners.entrySet()) {
							Class<T> cclass = vclass;
							if(entry.getKey().equals(vclass)){
								cclass = vclass;
							}else if(entry.getKey().equals(svclass)){
								cclass = svclass;
							}else if(entry.getKey().equals(svclass.getSuperclass())){
								cclass = toValue(svclass.getSuperclass());
								System.err.println(cclass.getSuperclass());
							}
							if(has){
								toInsert= getContainerListener(classlisteners, cclass).onUpdate(vg, v);
							}else{
								toInsert= getContainerListener(classlisteners, cclass).onInsert(v);
							}
						}
					}
				}
	    	}
			if(listeners.size()>0 && listeners.containsKey(k)){
				if(has(k)){
					T vg = (T)(values.get(k));
					toInsert= getContainerListener( listeners.get(k), vg).onUpdate(vg, v);
				}else{
					toInsert= getContainerListener( listeners.get(k), v).onInsert(v);
				}
			}
			values.put(k, toInsert);
		}
		return this;
	}
	public static <T>List<Class<T>> getClasses(T obj){
		List<Class<T>> out = new ArrayList<Class<T>>();
		Class<T> cclass = null;
		if(obj instanceof Class<?>){
			cclass = toValue(((Class<?>)obj));
		}else{
			cclass = toValue(obj.getClass());
		}
		do{
			out.add(cclass);
		}while(!(cclass = toValue(cclass.getSuperclass())).equals(Object.class));
		return out;
	}
	/**
	 * Pobiera dodatkową właściwość typu Object lub null jeżeli nie mamy dodatkowej właściwości
	 * @param k klucz
	 * @return get(key)::Object
	 */
	public <T>T get(String k, T def){
		T out = def;
		if(has(k)){
	    	out = (T)values.get(k);
	    	if(out==null){
	    		out=def;
	    	}
		}
		if(listener!=null){
			if(def==null){
				out= (T)(listener.onRead(out));
			}else{
				out= (T)(listener.onRead(out, def));
			}
		}
		if(classlisteners!=null){
			Class<T> vclass = getClass(out);
			if(vclass!=null){
				if(classlisteners.size()>0){
					Class<T> svclass = toValue(vclass.getSuperclass());
					for (Map.Entry<Class<?>, ContainerListener<?>> entry : classlisteners.entrySet()) {
						Class<T> cclass = vclass;
						if(entry.getKey().equals(vclass)){
							cclass = vclass;
						}else if(entry.getKey().equals(svclass)){
							cclass = svclass;
						}
			    		if(def==null){
			    			out= getContainerListener(classlisteners, cclass).onRead(out);
			    		}else{
			    			out= getContainerListener(classlisteners, cclass).onRead(out, def);
			    		}
					}
				}
			}
		}
		if(listeners.size()>0 && listeners.containsKey(k)){
			if(def==null){
				out=(T) getContainerListener(listeners.get(k), out).onRead(out);
			}else{
				out=(T) getContainerListener(listeners.get(k), out).onRead(out, def);
			}
		}
		return (T) out;
	}
	
	/**
	 * Usuwa obiekt z kontenera
	 * @param key klucz obiektu
	 * @return this
	 */
	public Container remove(String key){
		boolean toDelete = has(key); 
		if(listener!=null){
			toDelete = listener.onDelete(toDelete);
		}
		if(classlisteners!=null){
			Class<?> vclass = getClass(values.get(key));
			if(vclass!=null){
				Class<?> svclass = toValue(vclass.getSuperclass());
				for (Map.Entry<Class<?>, ContainerListener<?>> entry : classlisteners.entrySet()) {
					Class<?> cclass = vclass;
					if(entry.getKey().equals(vclass)){
						cclass = vclass;
					}else if(entry.getKey().equals(svclass)){
						cclass = svclass;
					}
					toDelete= classlisteners.get(cclass).onDelete(toDelete);
				}
			}
    	}
		if(listeners.size()>0 && listeners.containsKey(key)){
			toDelete = listeners.get(key).onDelete(toDelete);
		}
		if(toDelete){
			values.remove(key);
		}
		return this;
	}
	/**
	 * Sprawdza czy obiekt zawiera podaną dodatkową właściwość
	 * @param key klucz
	 * @return czy zawiera właściwość
	 */
	public <T>boolean has(String key){
		boolean out = key!=null && values.containsKey(key);
		if(listener!=null){
			out=listener.onCheck(out);
		}
		if(classlisteners!=null){
			Class<T> vclass = getClass(values.get(key));
			if(vclass!=null && classlisteners.containsKey(vclass)){
				Class<?> svclass = toValue(vclass.getSuperclass());
				for (Map.Entry<Class<?>, ContainerListener<?>> entry : classlisteners.entrySet()) {
					Class<?> cclass = vclass;
					if(entry.getKey().equals(vclass)){
						cclass = vclass;
					}else if(entry.getKey().equals(svclass)){
						cclass = svclass;
					}
					out= getContainerListener(classlisteners, cclass).onCheck(out);
				}
			}
		}
		if(listeners.size()>0 && listeners.containsKey(key)){
			out=getContainerListener(listeners.get(key), toValue(values.get(key))).onCheck(out);
		}
		return  out;
	}
	/**
	 * @param <T>
	 * @param cl
	 * @param key
	 * @return
	 */
	private <T>ContainerListener<T> getContainerListener(ContainerListener<?> cl, T key){
		if(cl!=null && equalsClass(getParameterizedType(cl) , getClass(key) )){
			return toValue(cl);
		}
		return new ContainerListenerAdapter<T>();
	}
	/**
	 * @param <T>
	 * @param m
	 * @param vclass
	 * @return
	 */
	private <T>ContainerListener<T> getContainerListener(Map<Class<?>, ContainerListener<?>> m, Class<T> vclass){
		ContainerListener<?> cl = m.get(vclass);
		if(cl!=null && equalsClass(getParameterizedType(cl) , vclass )){
			return toValue(cl);
		}
		return new ContainerListenerAdapter<T>();
	}
	/**
	 * @param o
	 * @return
	 */
	public static Type getParameterizedType(Object o){
		if(o!=null){
			Class<?> co = o.getClass();
			ParameterizedType pto = (ParameterizedType) co.getGenericSuperclass();
			return pto.getActualTypeArguments()[0];
		}
		return null;
	}  
	/**
	 * @param <T>
	 * @param key
	 * @return
	 */
	private <T>T getValue(String key){
		return (T)(values.get(key)); 
	}
	/**
	 * @param <T>
	 * @param o
	 * @return
	 */
	private static <T>T toValue(Object o){
		try{
			return (T)o;
		}catch(Exception e){return null;}
	}
	/**
	 * W pełni bezpieczna metoda.
	 * Klasa jest pobierana z przekazywanego obiektu
	 * @param <T>
	 * @param o
	 * @return Class
	 */
	public static <T>Class<T> getClass(Object o){
		if(o==null){
			return null;
		}
		return toValue(o.getClass());
	}
	/**
	 * @param o1
	 * @param o2
	 * @return boolean
	 */
	public static boolean equalsClass(Object o1, Object o2){
		boolean out = false;
		if(o1==null || o2==null){
			return false;
		}
		Class<?> c1 = o1.getClass();
		Class<?> c2 = o2.getClass();
		out = c1.equals(o2);
		return out||c1.getSuperclass().equals(c2.getSuperclass());
	}
	/**
	 * Zwraca mapę dodatkowych właściwości obiektu(nigdy null)
	 * @return mapa
	 */
	public Map<String, Object> get(){
		return values;
	}
}
